package org.zhaya.sdk.swamill;

import io.logz.sawmill.Doc;
import lombok.Getter;
import lombok.Setter;

import java.util.*;

import static com.google.common.base.Preconditions.checkState;

@Getter
@Setter
public class ProcessDoc extends Doc {

    public ProcessDoc(Map<String, Object> source) {
        super(source);
        this.source = source;
    }

    private final Map<String, Object> source;

    @Override
    public boolean hasField(String path) {
        return hasField(path, true);
    }

    /**
     * @param path
     * @param isTokenized path是否分词，如果不分词则将 path直接当成 Map 中的 key 进行处理，提升性能
     * @return
     */
    public boolean hasField(String path, boolean isTokenized) {
        Optional<Object> field = getByPath(source, isTokenized, path);
        return field.isPresent();
    }

    @Override
    public boolean hasField(String path, Class clazz) {
        Optional<Object> field = getByPath(source, path);
        return field.isPresent() && clazz.isInstance(field.get());
    }

    @Override
    public <T> T getField(String path) {
        return getField(path, true);
    }

    /**
     * @param path
     * @param isTokenized path是否分词，如果不分词则将 path直接当成 Map 中的 key 进行处理，提升性能
     * @param <T>
     * @return
     */
    public <T> T getField(String path, boolean isTokenized) {
        Optional<T> field = getByPath(source, isTokenized, path);
        checkState(field.isPresent(), "Couldn't resolve field in path [%s]", path);
        return field.get();
    }

    @Override
    public void addField(String path, Object value) {
        addField(path, value, true);
    }

    /**
     * @param path
     * @param value
     * @param isTokenized path是否分词，如果不分词则将 path直接当成 Map 中的 key 进行处理，提升性能
     */
    public void addField(String path, Object value, boolean isTokenized) {
        if (isTokenized) {
            Map<String, Object> context = source;
            List<String> pathElements = tokenizePath(path);
            String leafKey = pathElements.get(pathElements.size() - 1);
            for (String pathElement : pathElements.subList(0, pathElements.size() - 1)) {
                Object pathValue = context.get(pathElement);
                if (pathValue instanceof Map) {
                    context = (Map) pathValue;
                } else {
                    Map<String, Object> newMap = new HashMap<>();
                    context.put(pathElement, newMap);
                    context = newMap;
                }
            }
            context.put(leafKey, value);
        } else {
            source.put(path, value);
        }
    }

    @Override
    public boolean removeField(String path) {
        return removeField(path, true);
    }

    /**
     * removes field from source
     *
     * @param path
     * @param isTokenized path是否分词，如果不分词则将 path直接当成 Map 中的 key 进行处理，提升性能
     * @return {@code true} if field has been removed
     * {@code false} if field wasn't exist
     */
    public boolean removeField(String path, boolean isTokenized) {
        if (!hasField(path, isTokenized)) {
            return false;
        }
        if (isTokenized) {
            Map<String, Object> context = source;
            List<String> pathElements = tokenizePath(path);

            List<String> pathElementsWithoutLeaf = pathElements.subList(0, pathElements.size() - 1);
            String leafKey = pathElements.get(pathElements.size() - 1);

            for (String currElement : pathElementsWithoutLeaf) {
                context = (Map<String, Object>) context.get(currElement);
            }
            context.remove(leafKey);
        } else {
            source.remove(path);
        }
        return true;
    }

    @Override
    public void appendList(String path, Object value) {
        if (!hasField(path)) {
            addField(path, new ArrayList<>());
        }

        Object field = getField(path);
        List<Object> list;
        if (field instanceof List) {
            list = (List) field;
        } else {
            list = new ArrayList<>();
            list.add(field);
            removeField(path);
            addField(path, list);
        }
        if (value instanceof List) {
            list.addAll((List) value);
        } else {
            list.add(value);
        }
    }

    /**
     * removes value from a list
     *
     * @param path
     * @param value
     * @return {@code true} if value removed from list
     * {@code false} otherwise
     */
    @Override
    public boolean removeFromList(String path, Object value) {
        if (!hasField(path)) {
            return false;
        }

        Object field = getField(path);
        if (field instanceof List) {
            List<Object> list = (List) field;

            if (value instanceof List) {
                list.removeAll((List) value);
            } else {
                list.remove(value);
            }

            return true;
        }

        return false;
    }

    private static <T> Optional<T> getByPath(Map<String, Object> json, String... paths) {
        return getByPath(json, true, paths);
    }

    /**
     * json OGNL (Object Graph Navigation Language) getter.
     * <p>for example:
     * <pre>
     * JsonUtils.getByPath(json, "x.y.z")
     * </pre>
     * isTokenized 为 false 时，则直接将 Path 当成 Map中的 key 进行处理，加速处理性能
     *
     * @return Optional of the value in paths
     * @throws Exception on any error
     **/
    private static <T> Optional<T> getByPath(Map<String, Object> json, boolean isTokenized, String... paths) {
        Object cursor;
        if (isTokenized) {
            cursor = json;
            for (String path : paths) {
                for (String pathElement : tokenizePath(path)) {
                    if (!(cursor instanceof Map)) {
                        return Optional.empty();
                    }
                    cursor = ((Map) cursor).get(pathElement);
                    if (cursor == null) {
                        return Optional.empty();
                    }
                }

            }
        } else {
            cursor = json.get(paths[0]);
            if (cursor == null) {
                return Optional.empty();
            }
        }
        return Optional.of((T) cursor);
    }

    private static List<String> tokenizePath(String s) {
        List<String> pathTokens = new ArrayList<>(1);
        StringBuilder sb = new StringBuilder(s.length());
        boolean inEscape = false;

        for (char c : s.toCharArray()) {
            if (inEscape) {
                inEscape = false;
                sb.append(c);
            } else if (c == '\\') {
                inEscape = true;
            } else if (c == '.') {
                pathTokens.add(sb.toString());
                sb.setLength(0);
            } else {
                sb.append(c);
            }
        }
        pathTokens.add(sb.toString());

        return pathTokens;
    }

    @Override
    public boolean replaceFieldValue(String path, Object newValue) {
        if (hasField(path)) {
            removeField(path);
            addField(path, newValue);
            return true;
        }
        return false;
    }

}
